/*! @file menusys.cpp
    @version 1.6
    @brief menusys implementation
 */

#ifdef _WIN32
# include <windows.h>
# include <direct.h>
# include <process.h>
# ifndef MSYS_NOTHREADS
#  define THREAD_HANDLE          HANDLE
#  define THREAD_HANDLE_INIT(x)  x = NULL
# endif
#else
# include <unistd.h>
# include <sys/stat.h>
# ifndef MSYS_NOTHREADS
#  include <pthread.h>
#  define THREAD_HANDLE          pthread_t
#  define THREAD_HANDLE_INIT(x)
# endif
# include <limits.h>
# define MAX_PATH               PATH_MAX
#endif

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include <sys/timeb.h>
#include <sys/types.h>
#include <time.h>

#include <queue>
#include <set>
#include <map>
#include <algorithm>

#include "menusys.h"
#include "menusysu.h"
#ifdef _UNICODE
# include "ConvertUTF.h"
#endif

// ----------------------------------------------------------------------------
// STRING CONSTANTS
// ----------------------------------------------------------------------------

/*! String constants are a pain when using ICU on platforms that have wchar_t
    size different to the UChar size (i.e. Mac, Unix). On these platforms we
    need to construct the constants with some effort. On Windows we can just
    define them simply.
 */
#if defined(USING_ICU) && !defined(_WIN32)
#define StringConstant(name, str) \
    static const struct _str_ ## name { \
        static const size_t len = sizeof(str)-1; \
        static const UChar data[sizeof str]; \
        _str_ ## name() { \
            u_charsToUChars(str, (UChar*) data, sizeof str); \
        } \
        operator const UChar *() const { return data; } \
    } name
#else
# ifdef _UNICODE
#  define StringConstant(name, str) static const TCHAR name[] = L ## str;
# else
#  define StringConstant(name, str) static const TCHAR name[] = str;
# endif
#endif

// naming convention is to prefix the actual string with double underscore
// when it is a short word. Otherwise use a short name with double underscore.
static const TCHAR  OPTION_PREFIX[]   = _T("_setopt_");
static const size_t OPTION_PREFIX_LEN = (sizeof(OPTION_PREFIX) / sizeof(TCHAR)) - 1;

// the text of this isn't used, only the address which will be unique
extern const TCHAR * MSYS_HEADER = _T("");


// ----------------------------------------------------------------------------
// TYPE DEFINITIONS
// ----------------------------------------------------------------------------

/*! @brief Map the name of a procedure to the offset of that
    procedure in the script file.
 */
typedef std::map<std::tstring,long> ProcMap;

/*! @brief Stack of current procedure calls.
 */
typedef std::vector<long> ProcStack;

struct InputFile
{
    FILE *          mFile;
    std::tstring    mPath;
    bool            mIsUtf8;
    ProcMap         mProc;
    ProcStack       mProcStack;
#ifdef _WIN32
    HANDLE          mFileLock;
#endif

    InputFile(FILE * aFile = NULL, const TCHAR * aPath = _T(""), bool aIsUtf8 = false);
    InputFile(const InputFile & rhs);
    InputFile & operator=(const InputFile & rhs);
};

template <typename T, size_t INIT_SIZE = 1024>
struct BufferImpl
{
    T *     mBuf;
    size_t  mBufSiz;

    BufferImpl();
    ~BufferImpl();
    void Grow();
    void Format(const TCHAR * aFormat, ...);
    void FormatV(const TCHAR * aFormat, va_list ap);
};

typedef BufferImpl<char>    BufferA;
typedef BufferImpl<TCHAR>   Buffer;

typedef std::vector<long>   LapArray;

class Timer {
    struct timeb    mStart;
    long            mElapsed;
    bool            mRunning;
    LapArray        mLaps;

public:
    Timer();
    void Start(bool aReset = false);
    void Stop();
    void Reset();
    long GetCurrentElapsed() const;
    bool IsRunning() const;
    void Lap();
    const LapArray & Laps() const { return mLaps; }

private:
    long CalculateElapsed(struct timeb * aFinish = NULL) const;
};

typedef std::map<std::tstring,Timer> TimerMap;

enum DISPLAY {
    DISPLAY_NONE, DISPLAY_PROMPT, DISPLAY_OUTPUT, DISPLAY_ALL
};

typedef std::map<std::tstring,std::tstring> VarMap;

struct MSYS_ThreadImpl : public MSYS_Thread
{
    bool                    bStopThread;
    bool                    bIsRunning;
    std::tstring            mThreadName;
#ifndef MSYS_NOTHREADS
    THREAD_HANDLE           mThreadHandle;
#endif
    std::vector<InputFile>  mInputStack;
    InputFile               mInputCurr;
    FILE *                  mTeeFile;
    bool                    mUseNeedHelp;
    bool                    mUseTimestamp;
    DISPLAY                 mOptionConsole;
    Buffer                  mHandlerBuf;
    BufferA                 mCharBuf;
    bool                    mStringReplacements;

    MSYS_ThreadImpl(const TCHAR * aThreadName, const MSYS_ThreadImpl * aParent);
    ~MSYS_ThreadImpl();

public:
    void SetCurrCommand(const TCHAR * aCommand) { mCurrCommand = aCommand; }
    void LoopStartCounted(long aProcedure, int aCount);
    void LoopStartTimed(long aProcedure, time_t aExitTime);
    bool Loop();

private:
    long    mLoopProcedure;
    time_t  mLoopLimit;

    void LoopStart();
    bool LoopTestConditions();

private:
    MSYS_ThreadImpl(const MSYS_ThreadImpl & rhs); // disable
    MSYS_ThreadImpl & operator=(const MSYS_ThreadImpl & rhs); // disable
};

typedef std::map<std::tstring, MSYS_ThreadImpl*> ThreadMap;

#ifdef MSYS_NOTHREADS
class ThreadLock {
public:
    ThreadLock()    { }
    ~ThreadLock()   { }
    void Lock()     { }
    void Unlock()   { }
};
#else
# ifdef _WIN32
class ThreadLock {
    CRITICAL_SECTION m_lock;
public:
    ThreadLock()    { InitializeCriticalSection(&m_lock); }
    ~ThreadLock()   { DeleteCriticalSection(&m_lock); }
    void Lock()     { EnterCriticalSection(&m_lock); }
    void Unlock()   { LeaveCriticalSection(&m_lock); }
};
# else // !_WIN32
class ThreadLock {
    pthread_mutex_t m_lock;
public:
    ThreadLock()    { pthread_mutex_init(&m_lock, NULL); }
    ~ThreadLock()   { pthread_mutex_destroy(&m_lock); }
    void Lock()     { pthread_mutex_lock(&m_lock); }
    void Unlock()   { pthread_mutex_unlock(&m_lock); }
};
# endif // _WIN32
#endif // MSYS_NOTHREADS

struct AutoLock {
    ThreadLock & mLock;
    AutoLock(ThreadLock & aLock) : mLock(aLock) { mLock.Lock(); }
    ~AutoLock() { mLock.Unlock(); }

private:
    AutoLock(const AutoLock &); // disable
    AutoLock & operator=(const AutoLock &); // disable
};

enum PARSE { PARSE_DEFAULT, PARSE_ON, PARSE_OFF };


// ----------------------------------------------------------------------------
// TYPE IMPLEMENTATIONS
// ----------------------------------------------------------------------------

InputFile::InputFile(
    FILE *          aFile,
    const TCHAR *   aPath,
    bool            aIsUtf8
    )
    : mFile(aFile)
    , mPath(aPath)
    , mIsUtf8(aIsUtf8)
{ }

InputFile::InputFile(const InputFile & rhs)
{
    operator=(rhs);
}

InputFile &
InputFile::operator=(
    const InputFile & rhs
    )
{
    mFile       = rhs.mFile;
    mPath       = rhs.mPath;
    mIsUtf8     = rhs.mIsUtf8;
    mProc       = rhs.mProc;
    mProcStack  = rhs.mProcStack;
#ifdef _WIN32
    mFileLock   = rhs.mFileLock;
#endif

    return *this;
}

template <typename T, size_t INIT_SIZE>
BufferImpl<T,INIT_SIZE>::BufferImpl()
    : mBuf(NULL)
    , mBufSiz(INIT_SIZE)
{
    Grow();
}

template <typename T, size_t INIT_SIZE>
BufferImpl<T,INIT_SIZE>::~BufferImpl()
{
    if (mBuf) free(mBuf);
}

template <typename T, size_t INIT_SIZE>
void
BufferImpl<T,INIT_SIZE>::Grow()
{
    mBufSiz *= 2;
    mBuf = (T*) realloc(mBuf, mBufSiz * sizeof(T));
    if (!mBuf) {
        _ftprintf(stderr, _T("Out of memory.\n"));
        exit(1);
    }
}

template <typename T, size_t INIT_SIZE>
void
BufferImpl<T,INIT_SIZE>::Format(
    const TCHAR * aFormat,
    ...
    )
{
    va_list ap;
    va_start(ap, aFormat);
    FormatV(aFormat, ap);
    va_end(ap);
}

template <typename T, size_t INIT_SIZE>
void
BufferImpl<T,INIT_SIZE>::FormatV(
    const TCHAR *   aFormat,
    va_list         ap
    )
{
    int nLength = 0;
    do {
        if (nLength == -1) Grow();
        nLength = _vsntprintf(mBuf, mBufSiz, aFormat, ap);
    } while (nLength == -1);
}

Timer::Timer()
{
    mElapsed = 0;
    mStart.time = 0;
    mStart.millitm = 0;
    mRunning = false;
}

void
Timer::Start(
    bool    aReset
    )
{
    if (aReset) {
        Reset();
    }
    ftime(&mStart);
    mRunning = true;
}

void
Timer::Stop()
{
    if (mRunning) {
        mRunning = false;
        mElapsed += CalculateElapsed();
    }
}

void
Timer::Reset()
{
    mElapsed = 0;
    mRunning = false;
    mLaps.clear();
}

long
Timer::GetCurrentElapsed() const
{
    if (mRunning) {
        return mElapsed + CalculateElapsed();
    }
    return mElapsed;
}

bool
Timer::IsRunning() const
{
    return mRunning;
}

void
Timer::Lap()
{
    long nLapTime = mElapsed;
    mElapsed = 0;
    if (mRunning) {
        struct timeb timeEnd;
        ftime(&timeEnd);
        nLapTime += CalculateElapsed(&timeEnd);
        mStart = timeEnd;
    }
    mLaps.push_back(nLapTime);
}

long
Timer::CalculateElapsed(
    struct timeb * aFinish
    ) const
{
    struct timeb timeEnd;
    if (aFinish) {
        timeEnd = *aFinish;
    }
    else {
        ftime(&timeEnd);
    }
    time_t nPeriodSecs = timeEnd.time - mStart.time;
    int nPeriodMillis = (int) timeEnd.millitm - (int) mStart.millitm;
    return (long) (nPeriodSecs * 1000) + nPeriodMillis;
}

MSYS_ThreadImpl::MSYS_ThreadImpl(
    const TCHAR *           aThreadName,
    const MSYS_ThreadImpl * aParent
    )
    : bStopThread(false)
    , bIsRunning(true)
    , mThreadName(aThreadName)
    , mTeeFile(NULL)
    , mUseNeedHelp(true)
    , mUseTimestamp(false)
    , mOptionConsole(DISPLAY_ALL)
    , mStringReplacements(true)
    , mLoopProcedure(0)
    , mLoopLimit(0)
{
#ifndef MSYS_NOTHREADS
    THREAD_HANDLE_INIT(mThreadHandle);
#endif
    static int nNextThreadId = ROOT_THREAD;
    if (aParent == NULL) {
        nNextThreadId = ROOT_THREAD;
    }
    mThreadId = nNextThreadId++;
    mInputCurr = InputFile(stdin, _T("stdin"));
}

MSYS_ThreadImpl::~MSYS_ThreadImpl()
{
    if (mTeeFile) {
        fclose(mTeeFile);
        mTeeFile = NULL;
    }
    while (!mInputStack.empty()) {
        if (mInputStack.back().mFile != stdin) {
            fclose(mInputStack.back().mFile);
#ifdef _WIN32
            CloseHandle(mInputStack.back().mFileLock);
#endif
        }
        mInputStack.pop_back();
    }
    MSYS_ThreadUserDestroy(this);
}

void
MSYS_ThreadImpl::LoopStartCounted(
    long    aProcedure,
    int     aCount
    )
{
    assert(mInputCurr.mFile != stdin);

    if (aCount < 0) aCount = 0;
    mLoopProcedure = aProcedure;
    mLoopLimit = -aCount;
    if (!LoopTestConditions()) return;
    LoopStart();
}

void
MSYS_ThreadImpl::LoopStartTimed(
    long    aProcedure,
    time_t  aExitTime
    )
{
    assert(mInputCurr.mFile != stdin);

    mLoopProcedure = aProcedure;
    mLoopLimit = aExitTime;
    if (!LoopTestConditions()) return;
    LoopStart();
}

bool
MSYS_ThreadImpl::Loop()
{
    assert(mInputCurr.mFile != stdin);
    if (!LoopTestConditions()) return false;

    // no exit yet, go back to the beginning of the procedure
    if (fseek(mInputCurr.mFile, mLoopProcedure, SEEK_SET) != 0) {
        _ftprintf(stderr, _T("Failed loop procedure call. Exiting loop.\n"));
        mLoopLimit = 0;
        return false;
    }

    return true;
}

void
MSYS_ThreadImpl::LoopStart()
{
    assert(mInputCurr.mFile != stdin);

    // call the procedure
    long lCurr = ftell(mInputCurr.mFile);
    if (fseek(mInputCurr.mFile, mLoopProcedure, SEEK_SET) != 0) {
        _ftprintf(stderr, _T("Failed loop procedure call. Exiting loop.\n"));
        mLoopLimit = 0;
        return;
    }

    // push our return value
    mInputCurr.mProcStack.push_back(lCurr);
}

bool
MSYS_ThreadImpl::LoopTestConditions()
{
    assert(mInputCurr.mFile != stdin);
    if (mLoopLimit == 0) return false;

    // check our loop exit status
    if (mLoopLimit < 0) {
        // negative values are counter limits
        if (mLoopLimit++ == 0) return false;
    }
    else {
        // positive values are time limits
        time_t tNow;
        time(&tNow);
        if (tNow >= mLoopLimit) {
            mLoopLimit = 0;
            return false;
        }
    }

    return true;
}

bool MSYS_DispatchEntry::operator==(const MSYS_DispatchEntry & rhs) const {
    return _tcsicmp(cmd, rhs.cmd) == 0;
}


// ----------------------------------------------------------------------------
// LOCAL FUNCTION DEFINITIONS
// ----------------------------------------------------------------------------

// general functions
static void InitInternal();
static bool FileExists(const TCHAR * aFilePath);
static bool StringReplacements(MSYS_ThreadImpl * aThread, Buffer & aBuf);
#ifdef _WIN32
static void InputFromConsole(Buffer & aBuf);
#endif // _WIN32
static void OutputToConsole(const TCHAR * aString);
static void OutputToFile(MSYS_ThreadImpl * aThread, FILE * aFile, const Buffer & aBuf);
static void DisplayTimestamp(MSYS_ThreadImpl * aThread, const TCHAR * aType);

// dispatch loop
static bool Input(MSYS_ThreadImpl * aThread, Buffer & aBuf);
static void Parse(TCHAR * aBuf, TCHAR *& aCommand, TCHAR *& aArgs);
static void ParseArgs(TCHAR * aBuf, bool aUnquote, MSYS_Args & aArgs);
static bool Dispatch(MSYS_ThreadImpl * aThread, TCHAR * aCommand, TCHAR * aArgs, PARSE aParse);

// internal command handlers
static void DoChdir(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoEcho(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoExit(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoHelp(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoLoop(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoProc(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoPwd(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoRead(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoSetOpt(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoSetVar(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoShell(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoSleep(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoTee(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void DoTimer(MSYS_Thread * thrd, MSYS_Args & aArgs);
#ifndef MSYS_NOTHREADS
static void DoThreads(MSYS_Thread * thrd, MSYS_Args & aArgs);
#endif // MSYS_NOTHREADS

// internal option handlers
static void OptConsole(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void OptThreads(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void OptUtf8(MSYS_Thread * thrd, MSYS_Args & aArgs);
static void OptTimestamp(MSYS_Thread * thrd, MSYS_Args & aArgs);

// ----------------------------------------------------------------------------
// LOCAL VARIABLE DEFINITIONS
// ----------------------------------------------------------------------------

extern const MSYS_DispatchEntry g_rgInternal[]; // forward declaration

static MSYS_DispatchEntry * g_rgDispatch = NULL;
static bool                 g_bEnableThreads = false;
static ThreadMap            g_mapThreads;
static ThreadLock           g_lockThreadsMap;
static ThreadLock           g_lockOutputConsole;
static ThreadLock           g_lockOutputTeeFile;
static MSYS_ThreadImpl *    g_pRootThread = NULL;
static ThreadLock           g_lockTimerAndVars;
static TimerMap             g_mapTimers;
static VarMap               g_mapVariables;


// ----------------------------------------------------------------------------
// FUNCTION DEFINITIONS
// ----------------------------------------------------------------------------

/*! @brief Main thread function running the command input and dispatch loop.

    Because this is a real thread function it has a different signature
    depending on the platform this is compiled for.
 */
#ifdef _WIN32
static unsigned __stdcall InputLoop(void * thrd)
#else
static void * InputLoop(void * thrd)
#endif
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    Buffer inputBuffer;
    TCHAR *pCommand, *pArgs;

    // menu dispatch loop
    TCHAR szPrompt[200];
    bool bInCommentBlock = false;
    bool bStringReps = false;
    for (bool isOK = true; isOK && !aThread->bStopThread; bStringReps = false) {
        MSYS_GetPrompt(aThread, szPrompt, sizeof(szPrompt)/sizeof(szPrompt[0]));
        if (aThread->mInputCurr.mFile == stdin) {
            if (aThread->mTeeFile) fflush(aThread->mTeeFile);
            _ftprintf(stdout, _T("%s"), szPrompt);
            Input(aThread, inputBuffer);
            bStringReps = StringReplacements(aThread, inputBuffer);
        }
        else {
            // when the current input file is finished (or returns an error)
            // close it and return to the previous one
            if (!Input(aThread, inputBuffer)) {
                if (!aThread->mInputCurr.mProcStack.empty()) {
                    MSYS_Display(aThread,
                        _T("WARNING: procedure stack not empty at end of file.\n"));
                }
                fclose(aThread->mInputCurr.mFile);
#ifdef _WIN32
                CloseHandle(aThread->mInputCurr.mFileLock);
#endif
                isOK = !aThread->mInputStack.empty();
                if (isOK) {
                    aThread->mInputCurr = aThread->mInputStack.back();
                    aThread->mInputStack.pop_back();
                }
                bInCommentBlock = false;
                continue;
            }
            bStringReps = StringReplacements(aThread, inputBuffer);
        }

        Parse(inputBuffer.mBuf, pCommand, pArgs);
        if (_tcsicmp(pCommand, _T("*/")) == 0) {
            bInCommentBlock = false;
            continue;
        }
        if (bInCommentBlock || *pCommand == ';' || *pCommand == '#') { // comments
            continue;
        }
        if (_tcsicmp(pCommand, _T("/*")) == 0) {
            bInCommentBlock = true;
            continue;
        }

        // remove special prefixes. shell escape (!) handled separately.
        // @ no echo, $ time command, = no help, - no parse, + force parse
        const TCHAR szPrefix[] = _T("@$=-+");
        enum { NO_ECHO, TIMING, NO_HELP, NO_PARSE, USE_PARSE, COUNT };
        assert(_tcslen(szPrefix) == COUNT);
        bool rgFlags[COUNT] = { false };
        const TCHAR * pItem = _tcschr(szPrefix, *pCommand);
        while (pItem && *pCommand) {
            ++pCommand;
            rgFlags[pItem - szPrefix] = true;
            pItem = _tcschr(szPrefix, *pCommand);
        }
        if (!*pCommand) { // if we had a space before the command
            Parse(pArgs, pCommand, pArgs);
        }

        // process the command
        Timer lineTimer;
        aThread->mUseNeedHelp = !rgFlags[NO_HELP];
        if (rgFlags[TIMING]) {
            lineTimer.Start(true);
        }
        if (aThread->mOptionConsole == DISPLAY_ALL || aThread->mOptionConsole == DISPLAY_PROMPT) {
            if (!rgFlags[NO_ECHO]) {
                if (bStringReps || aThread->mInputCurr.mFile != stdin) {
                    MSYS_Display(aThread, _T("%s%s %s\n"), szPrompt, pCommand, pArgs);
                }
                else if (aThread->mTeeFile) {
                    Buffer buf;
                    buf.Format(_T("%s%s %s\n"), szPrompt, pCommand, pArgs);
                    OutputToFile(aThread, aThread->mTeeFile, buf);
                }
            }
        }
        if (*pCommand) {
            PARSE p = PARSE_DEFAULT;
            if (rgFlags[NO_PARSE])
                p = PARSE_OFF;
            else if (rgFlags[USE_PARSE])
                p = PARSE_ON;
            if (aThread->mUseTimestamp) {
                DisplayTimestamp(aThread, _T("Begin:"));
            }
            isOK = Dispatch(aThread, pCommand, pArgs, p);
            if (aThread->mUseTimestamp) {
                DisplayTimestamp(aThread, _T("End:  "));
            }
        }
        if (rgFlags[TIMING]) {
            lineTimer.Stop();
            long nElapsed = lineTimer.GetCurrentElapsed();
            if (aThread->mOptionConsole == DISPLAY_PROMPT) {
                // force console output for this
                TCHAR szResults[100];
                _sntprintf(szResults, 100, _T("Elapsed time %ld.%03ld seconds.\n"),
                    nElapsed / 1000, nElapsed % 1000);
                OutputToConsole(szResults);
            }
            else {
                MSYS_Display(aThread, _T("Elapsed time %ld.%03ld seconds.\n"),
                    nElapsed / 1000, nElapsed % 1000);
            }
        }
        aThread->mUseNeedHelp = true;
    }

    aThread->bIsRunning = false;
    return 0;
}

extern bool
MSYS_Process(
    const TCHAR *   aInputFile,
    bool            aEnableThreads
    )
{
    // make sure no-one starts us multiple times in different threads
    static bool bIsRunning = false;
    assert(!bIsRunning);
    if (bIsRunning) return false;
    bIsRunning = true;

    // create our main thread
    InitInternal();
    g_pRootThread = new MSYS_ThreadImpl(_T("_main_"), NULL);
    if (!g_pRootThread) {
        _ftprintf(stderr, _T("Out of memory.\n"));
        exit(1);
    }
    MSYS_ThreadUserCreate(g_pRootThread, NULL);
    assert(g_pRootThread->GetThreadId() == MSYS_Thread::ROOT_THREAD);

#ifdef MSYS_NOTHREADS
    if (aEnableThreads) {
        _ftprintf(stderr, _T("Threads cannot be enabled. Thread support is compiled out.\n"));
    }
    g_bEnableThreads = false;
#else
    g_bEnableThreads = aEnableThreads;
#endif // MSYS_NOTHREADS

    // prepare the main thread input file if we have one
    bool bRunInputLoop = true;
    MSYS_Args args;
    if (aInputFile) {
        args.resize(1);
        // DoRead never modifies the filename, so removing the const is safe...
        args[0] = const_cast<TCHAR*>(aInputFile);
        DoRead(g_pRootThread, args);
        if (g_pRootThread->mInputCurr.mFile == stdin) { // failed
            bRunInputLoop = false;
        }
        else {
            // don't return to stdin
            g_pRootThread->mInputStack.pop_back();
        }
    }
    if (bRunInputLoop) {
        // load the default properties file. Success or failure doesn't
        // matter for this file.
        if (FileExists(MSYS_DefaultOptionsFile)) {
            args.resize(1);
            // DoRead never modifies the filename, so removing the const is safe...
            args[0] = const_cast<TCHAR*>(MSYS_DefaultOptionsFile);
            DoRead(g_pRootThread, args);
        }

        // run the menu
        InputLoop(g_pRootThread);
    }

    // wait for and clear all threads before returning
#ifndef MSYS_NOTHREADS
    if (g_bEnableThreads) {
        args.clear();
        args.push_back(_T("joinall"));
        DoThreads(g_pRootThread, args);
    }
    assert(g_mapThreads.empty());
#endif // MSYS_NOTHREADS

    // shutdown
    free(g_rgDispatch);
    g_rgDispatch = NULL;
    delete g_pRootThread;
    g_pRootThread = NULL;

    bIsRunning = false;
    return true;
}

static bool
Input(
    MSYS_ThreadImpl *   aThread,
    Buffer &            aBuf
    )
{
#ifdef _WIN32
    if (aThread->mInputCurr.mFile == stdin) {
        InputFromConsole(aBuf);
        return true;
    }
#endif

    char * pBuf;
    size_t nBufSiz;
    size_t nBufLen;

    // always read a line in single byte chars from the file. We do this
    // knowing that UTF-8 and most MBCS encodings will not confuse an end of
    // line with any other character.
#ifdef _UNICODE
    long lPos = ftell(aThread->mInputCurr.mFile);
#endif
    std::string sLine;
    bool bRetry = false;
    do {
#ifdef _UNICODE
        pBuf    = aThread->mCharBuf.mBuf;
        nBufSiz = aThread->mCharBuf.mBufSiz;
#else
        pBuf    = aBuf.mBuf;
        nBufSiz = aBuf.mBufSiz;
#endif

        bRetry = false;
        if (!fgets(pBuf, (int)nBufSiz, aThread->mInputCurr.mFile)) {
            if (ferror(aThread->mInputCurr.mFile)) {
                MSYS_Display(aThread, _T("Error while reading input file: %s\n"),
                    aThread->mInputCurr.mPath.c_str());
            }
            return false;
        }

        // ensure that we have a complete line
        nBufLen = strlen(pBuf);
        if (nBufLen >= nBufSiz - 3) { // CR LF NULL
#ifdef _UNICODE
            if (fseek(aThread->mInputCurr.mFile, lPos, SEEK_SET) != 0) {
                MSYS_Display(aThread, _T("Failed file seek for re-read of line.\n"));
                return false;
            }

            // resize our input buffer
            aThread->mCharBuf.Grow();
            bRetry = true;
#else
            MSYS_Display(aThread, _T("Input line is too long.\n"));
            return false;
#endif
        }

        if (!bRetry) {
            // trim trailing whitespace from the line
            int nLen = (int) nBufLen - 1;
            for (; nLen >= 0 && _istspace(pBuf[nLen]); ) {
                pBuf[nLen--] = 0;
            }

            // if we have a continuation character as the last char in the line, 
            // remove it and read the next line
            if (nLen >= 0 && pBuf[nLen] == '\\') {
                pBuf[nLen] = 0;
                sLine += pBuf;
                bRetry = true;
            }
        }
    }
    while (bRetry);

    // create the full line
    if (!sLine.empty()) {
        sLine += pBuf;
        nBufLen = sLine.length();

#ifdef _UNICODE
        while (nBufLen >= aThread->mCharBuf.mBufSiz) {
            aThread->mCharBuf.Grow();
        }
        strcpy(aThread->mCharBuf.mBuf, sLine.c_str());
        pBuf = aThread->mCharBuf.mBuf;
#else
        if (nBufLen >= aBuf.mBufSiz) { 
            MSYS_Display(aThread, _T("Input line is too long.\n"));
            return false;
        }
        strcpy(aBuf.mBuf, sLine.c_str());
        pBuf = aBuf.mBuf;
#endif
    }

#ifdef _UNICODE
    while (aBuf.mBufSiz < nBufLen) {
        aBuf.Grow();
    }

    // convert to Unicode
    if (aThread->mInputCurr.mIsUtf8) {
        const UTF8 *pIn = (UTF8*)pBuf, *pInEnd = (UTF8*)(pBuf + nBufLen + 1);
        UTF16 *pOut = (UTF16*)aBuf.mBuf, *pOutEnd = (UTF16*)(aBuf.mBuf + aBuf.mBufSiz);
        ConversionResult rc = ConvertUTF8toUTF16(
            &pIn, pInEnd, &pOut, pOutEnd, lenientConversion);
        if (rc != conversionOK) {
            MSYS_Display(aThread, _T("UTF-8 encoding error reading input file: %s\n"),
                aThread->mInputCurr.mPath.c_str());
            return false;
        }
    }
    else {
        size_t nCount = mbstowcs(aBuf.mBuf, pBuf, aBuf.mBufSiz);
        if (nCount == (size_t) -1) {
            MSYS_Display(aThread, _T("Native encoding error reading input file: %s\n"),
                aThread->mInputCurr.mPath.c_str());
            return false;
        }
        if (nCount == aBuf.mBufSiz) {
            MSYS_Display(aThread, _T("Line too long while reading input file: %s\n"),
                aThread->mInputCurr.mPath.c_str());
            return false;
        }
    }
#endif

    return true;
}

static void
Parse(
    TCHAR *  aBuf,
    TCHAR *& aCommand,
    TCHAR *& aArgs
    )
{
    // skip leading whitespace
    while (_istspace(*aBuf)) ++aBuf;

    // found start of command
    aCommand = aBuf;

    // find end of command
    while (*aBuf && !_istspace(*aBuf)) ++aBuf;

    // no arguments
    if (!*aBuf) {
        aArgs = aBuf;
        return;
    }

    // terminate command
    *aBuf++ = 0;
    aArgs = aBuf;

    // trim trailing spaces from the args
    aBuf += _tcslen(aBuf);
    for (--aBuf; aBuf >= aArgs && _istspace(*aBuf); --aBuf) {
        *aBuf = 0;
    }
}

static void
ParseArgs(
    TCHAR *     aBuf,
    bool        aFullParse,
    MSYS_Args & aArgs
    )
{
    aArgs.clear();

    bool isDone;
    do {
        // skip leading whitespace
        while (_istspace(*aBuf)) ++aBuf;
        if (!*aBuf) break; // done

        // start of arg
        TCHAR * pArg = aBuf;

        // There is a special interpretation of backslash characters when they are
        // followed by a quotation mark character ("), as follows:
        //
        // * 2n backslashes followed by a quotation mark produce n backslashes followed
        //      by a quotation mark.
        // * (2n) + 1 backslashes followed by a quotation mark again produce n
        //      backslashes followed by a quotation mark.
        // * n backslashes not followed by a quotation mark simply produce n backslashes.
        //
        // This is required to allow quotation marks to be quoted in a string. Yes,
        // it is also a pain in the arse.
        //
        TCHAR * pOut = aBuf;
        bool inQuotes = false;
        for (;;) {
            if (!*aBuf) {
                break;
            }
            if (!inQuotes && (*aBuf == ' ' || *aBuf == '\t')) {
                break;
            }
            if (*aBuf == '"') {
                inQuotes = !inQuotes;
                aBuf++;
                continue;
            }
            if (*aBuf != '\\') {
                *pOut++ = *aBuf++;
                continue;
            }

            // need to know if we are using quoted (2n=n) or unquoted (n=n)
            if (*aBuf == '\\') {
                TCHAR * pLast = aBuf + 1;
                while (*pLast == '\\') ++pLast;
                if (*pLast != '"') {
                    // non-quoting backslash, n=n, then continue normally
                    while (*aBuf == '\\') {
                        *pOut++ = *aBuf++;
                    }
                    continue;
                }

                // quoting backslash, 2n=n, then continue normally
                while (*aBuf == '\\') {
                    if (!*++aBuf) break;
                    *pOut++ = *aBuf;
                    if (*aBuf++ != '\\') break;
                }
                continue;
            }
        }

        isDone = (*aBuf == 0);
        *pOut = 0;
        ++aBuf;

        // add arg to arg array
        aArgs.push_back(pArg);

        // parse this arg to remove quoted chars
        if (aFullParse) {
            TCHAR * out = pArg;
            while (*pArg) {
                // full parse means that we turn known escape sequences into the
                // special characters.
                if (*pArg == '\\') {
                    ++pArg;
                    switch (*pArg) {
                    case 'n':  *out++ = '\n'; pArg++; break;
                    case 'r':  *out++ = '\r'; pArg++; break;
                    case 't':  *out++ = '\t'; pArg++; break;
                    default:
                        *out++ = '\\';
                        *out++ = *pArg++;
                    }
                }
                else {
                    *out++ = *pArg++;
                }
            }
            *out = 0;
        }
    }
    while (!isDone);
}

static void DoExit(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoExit, aArgs)) return;

    TCHAR * pAction = aArgs.size() >= 1 ? aArgs[0] : NULL;
    if (!pAction) return;

    if (_tcsicmp(_T("now"), pAction) == 0) {
#ifndef MSYS_NOTHREADS
        MSYS_Args args;
        args.push_back(_T("stopall"));
        DoThreads(aThread, args);
#endif // MSYS_NOTHREADS

        g_pRootThread->bStopThread = true;
        return;
    }

    MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
}

static bool ReplaceString(Buffer & aBuf, TCHAR * aBegin, TCHAR * aEnd, const TCHAR * aReplace)
{
    int nBuf     = (int) _tcslen(aBuf.mBuf);
    int nSearch  = (int) (aEnd - aBegin);
    int nReplace = (int) _tcslen(aReplace);
    int nDiff    = nReplace - nSearch;

    while (nBuf + nReplace - nSearch >= (int) aBuf.mBufSiz) {
        aBuf.Grow();
    }
    if (nDiff != 0) {
        int nRemaining = (int) (nBuf - (aBegin - aBuf.mBuf) - nSearch);
        memmove(aBegin + nSearch + nDiff, aBegin + nSearch,
            nRemaining * sizeof(TCHAR));
        aBegin[nSearch + nDiff + nRemaining] = 0;
    }
    memcpy(aBegin, aReplace, nReplace * sizeof(TCHAR));
    return true;
}

static TCHAR * my_strrstr(TCHAR * aBuf, const TCHAR * aFind) {
    TCHAR * pFound = _tcsstr(aBuf, aFind);
    if (!pFound) return NULL;

    size_t n = _tcslen(aFind);
    TCHAR * pNext = _tcsstr(pFound + n, aFind);
    while (pNext) {
        pFound = pNext;
        pNext = _tcsstr(pFound + n, aFind);
    }
    return pFound;
}

static bool StringReplacements(MSYS_ThreadImpl * aThread, Buffer & aBuf)
{
    if (!aThread->mStringReplacements) return false;

    const size_t nBufSiz = 200;
    TCHAR szBuf[nBufSiz];
    bool bMadeReplacement = false;

    TCHAR * pVarBegin = my_strrstr(aBuf.mBuf, _T("{{"));
    while (pVarBegin) {
        TCHAR * pVarEnd = _tcsstr(pVarBegin, _T("}}"));
        if (!pVarEnd) break;

        pVarEnd += 2;
        size_t nVarLen = pVarEnd - pVarBegin;

        bool bFoundThis = false;
        if (!_tcsncmp(pVarBegin, _T("{{TID}}"), nVarLen)) {
            _sntprintf(szBuf, nBufSiz, _T("%d"), aThread->GetThreadId());
            bFoundThis = ReplaceString(aBuf, pVarBegin, pVarEnd, szBuf);
        }
        else if (!_tcsncmp(pVarBegin, _T("{{TNAME}}"), nVarLen)) {
            bFoundThis = ReplaceString(aBuf, pVarBegin, pVarEnd, aThread->mThreadName.c_str());
        }
        else if (!_tcsncmp(pVarBegin, _T("{{NOW}}"), nVarLen)) {
            time_t tNow;
            time(&tNow);
            struct tm tmNow = *localtime(&tNow);
            _sntprintf(szBuf, nBufSiz, _T("%02d/%02d/%04d %02d:%02d:%02d"),
                tmNow.tm_mday, tmNow.tm_mon+1, tmNow.tm_year+1900,
                tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec);
            bFoundThis = ReplaceString(aBuf, pVarBegin, pVarEnd, szBuf);
        }
        else {
            AutoLock lock(g_lockTimerAndVars);
            VarMap::iterator i = g_mapVariables.begin();
            for (; !bFoundThis && i != g_mapVariables.end(); ++i) {
                _sntprintf(szBuf, nBufSiz, _T("{{%s}}"), i->first.c_str());
                if (!_tcsncmp(pVarBegin, szBuf, nVarLen)) {
                    bFoundThis = ReplaceString(aBuf, pVarBegin, pVarEnd, i->second.c_str());
                }
            }
        }

        if (!bFoundThis) {
            _tcsncpy(szBuf, pVarBegin, nVarLen);
            szBuf[nVarLen] = 0;
            bFoundThis = ReplaceString(aBuf, pVarBegin, pVarEnd, _T(""));
        }

        bMadeReplacement = true;
        pVarBegin = my_strrstr(aBuf.mBuf, _T("{{"));
    }

    return bMadeReplacement;
}

static void OutputToFile(MSYS_ThreadImpl * aThread, FILE * aFile, const Buffer & aBuf)
{
    if (!aFile) return;

#ifdef _UNICODE
    // output is always in UTF-8
    while (aThread->mCharBuf.mBufSiz <= aBuf.mBufSiz) {
        aThread->mCharBuf.Grow();
    }
    UTF8 *pOut    = (UTF8 *) aThread->mCharBuf.mBuf;
    UTF8 *pOutEnd = (UTF8 *) aThread->mCharBuf.mBuf + aThread->mCharBuf.mBufSiz;
    size_t nLen = _tcslen(aBuf.mBuf) + 1;
    const UTF16 *pIn    = (UTF16*)  aBuf.mBuf;
    const UTF16 *pInEnd = (UTF16*) (aBuf.mBuf + nLen);
    ConversionResult rc = ConvertUTF16toUTF8(
        &pIn, pInEnd, &pOut, pOutEnd, lenientConversion);
    if (rc != conversionOK) {
        _ftprintf(stderr, _T("UTF-8 encoding error writing output file (%s)\n"), 
            rc == sourceExhausted ? _T("partial character in source") :
                (rc == sourceIllegal ? _T("invalid character in source") 
                    : _T("buffer overflow"))
            );
        return;
    }
    size_t nBytes = (pOut - (UTF8*)aThread->mCharBuf.mBuf) - 1;
    AutoLock lock(g_lockOutputTeeFile);
    if (nBytes != fwrite(aThread->mCharBuf.mBuf, 1, nBytes, aFile)) {
        _ftprintf(stderr, _T("Error writing output file\n"));
        return;
    }
#else // _UNICODE
    AutoLock lock(g_lockOutputTeeFile);
    _ftprintf(aFile, _T("%s"), aBuf.mBuf);
#endif
}

extern void MSYS_OutputToFile(MSYS_Thread * thrd, FILE * aFile, const TCHAR * aFormat, ...)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    // generate the output string
    va_list ap;
    va_start(ap, aFormat);
    aThread->mHandlerBuf.FormatV(aFormat, ap);
    va_end(ap);

    StringReplacements(aThread, aThread->mHandlerBuf);
    OutputToFile(aThread, aFile, aThread->mHandlerBuf);
}

static void DisplayTimestamp(MSYS_ThreadImpl * aThread, const TCHAR * aType)
{
    struct timeb tb;
    ftime(&tb);
    struct tm * ptm = localtime(&tb.time);
    MSYS_Display(aThread, _T("%s %04u-%02u-%02u %02u:%02u:%02u.%03u\n"),
        aType,
        ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
        ptm->tm_hour, ptm->tm_min, ptm->tm_sec, tb.millitm
        );
}

extern void MSYS_Display(MSYS_Thread * thrd, const TCHAR * aFormat, ...)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    // generate the output string
    va_list ap;
    va_start(ap, aFormat);
    aThread->mHandlerBuf.FormatV(aFormat, ap);
    va_end(ap);

    StringReplacements(aThread, aThread->mHandlerBuf);

    if (aThread->mOptionConsole >= DISPLAY_OUTPUT) {
        // DISPLAY_OUTPUT, DISPLAY_ALL
        // DISPLAY_PROMPT will be handled where we handle the prompt
        OutputToConsole(aThread->mHandlerBuf.mBuf);
    }
    if (aThread->mTeeFile) {
        OutputToFile(aThread, aThread->mTeeFile, aThread->mHandlerBuf);
    }
}

extern void MSYS_DisplayWrapped(MSYS_Thread * thrd, const TCHAR * aText)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    assert(aText);

    std::tstring sText(aText);
    size_t n = sText.find(_T('\t'));
    while (n != std::tstring::npos) {
        sText.replace(n, 1, _T("    "));
        n = sText.find(_T('\t'), n + 3);
    }
    aText = sText.c_str();

    const int MAX_LINE_LEN = 79;
    int nLineLen, nStartIdx, nCurrIdx = 0, nLastBreakIdx = -1;
    while (nCurrIdx < (int) sText.size()) {
        nLineLen = 0;
        nStartIdx = nCurrIdx = nLastBreakIdx + 1;

        // find the end of this line
        while (nCurrIdx < (int) sText.size()
            && (nLastBreakIdx+1 == nStartIdx || nLineLen < MAX_LINE_LEN))
        {
            if (_istspace(sText[nCurrIdx])) {
                nLastBreakIdx = nCurrIdx;
                if (sText[nCurrIdx] == _T('\n')) {
                    break; // line break
                }
            }

            ++nLineLen;
            ++nCurrIdx;
        }
        if (nCurrIdx >= (int) sText.size()) {
            nLastBreakIdx = nCurrIdx;
        }

        // display the line
        std::tstring sLine(sText, nStartIdx, nLastBreakIdx - nStartIdx);
        MSYS_Display(aThread, _T("%s\n"), sLine.c_str());
    }
}

extern bool MSYS_NeedHelp(MSYS_Thread * thrd, MSYS_Handler fn, MSYS_Args & aArgs, bool aShowHelp)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (aThread->mUseNeedHelp) {
        for (size_t n = 0; n < aArgs.size(); ++n) {
            if (!_tcsicmp(aArgs[n], _T("-?")) || !_tcsicmp(aArgs[n], _T("/?"))) {
                if (aShowHelp) {
                    MSYS_ShowHelp(aThread, fn);
                }
                return true;
            }
        }
    }
    return false;
}

// dummy functions for MSYS_ShowHelp
static MSYS_Handler HelpExternal = NULL;
static MSYS_Handler HelpInternal = (MSYS_Handler) 1;
static MSYS_Handler HelpAll      = (MSYS_Handler) 2;

static void DoHelp(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoHelp, aArgs)) return;

    if (aArgs.empty()) {
        MSYS_ShowHelp(aThread, HelpExternal);
        return;
    }

    // check for help options
    if (aArgs[0][0] == '/') {
        TCHAR ch = (TCHAR) _totlower(aArgs[0][1]);
        if (ch == 'i') {
            MSYS_ShowHelp(aThread, HelpInternal);
        }
        else if (ch == 'a') {
            MSYS_ShowHelp(aThread, HelpAll);
        }
        else {
            MSYS_Display(aThread, _T("Unknown command.\n"));
        }
        return;
    }

    // should be a command
    const TCHAR * cmd = aArgs[0];
    for (int n = 0; g_rgDispatch[n].cmd; ++n) {
        if (_tcsicmp(cmd, g_rgDispatch[n].cmd) == 0) {
            // call the function to display the extended help
            aArgs.clear();
            if (g_rgDispatch[n].fn == DoShell) {
                aArgs.push_back(_T(""));
            }
            aArgs.push_back(_T("-?"));
            g_rgDispatch[n].fn(aThread, aArgs);
            return;
        }
    }

    // unknown command
    MSYS_Display(aThread, _T("Unknown command.\n"));
}

static void DoSleep(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoSleep, aArgs)) return;

    if (aArgs.size() != 1) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    int nMillis = _ttoi(aArgs[0]);
    if (nMillis < 0) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

#ifdef _WIN32
    Sleep((DWORD)nMillis);
#else
    usleep(nMillis * 1000);
#endif
}

static void DoEcho(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoEcho, aArgs)) return;

    for (size_t n = 0; n < aArgs.size(); ++n) {
        MSYS_Display(aThread, _T("%s"), aArgs[n]);
    }
    MSYS_Display(aThread, _T("\n"));
}

static void DoPwd(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoPwd, aArgs)) return;

    TCHAR buf[MAX_PATH];
    if (_tgetcwd(buf, sizeof(buf))) {
        MSYS_Display(aThread, _T("%s\n"), buf);
    }
}

static void DoChdir(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoChdir, aArgs)) return;

    if (aArgs.size() != 1) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    const TCHAR * pPath = aArgs[0];
    if (_tchdir(pPath) != 0) {
        if (errno == ENOENT) {
            MSYS_Display(aThread, _T("Invalid directory.\n"));
        }
        else {
            MSYS_Display(aThread, _T("chdir() failed.\n"));
        }
        return;
    }
}

static void DoShell(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    assert(aArgs.size() == 2);

    const TCHAR * pCommand = aArgs[0];
    const TCHAR * pArgs = aArgs[1];

    // equivalent to MSYS_NeedHelp, but we only recognize "! -?" and "! /?"
    if (!*pCommand && (!_tcsicmp(pArgs, _T("-?")) || !_tcsicmp(pArgs, _T("/?")))) {
        MSYS_ShowHelp(aThread, DoShell);
        return;
    }

    fflush(stdout);
    fflush(stderr);

    std::tstring sCommandLine;
    if (!*pCommand && !*pArgs) {
#ifdef _WIN32
        pCommand = _tgetenv(_T("COMSPEC"));
        if (!pCommand) {
            pCommand = _T("cmd.exe");
        }
#else
        pCommand = _tgetenv(_T("SH"));
        if (!pCommand) {
            pCommand = _T("/bin/sh");
        }
#endif
    }

    sCommandLine = pCommand;
    sCommandLine += _T(' ');
    sCommandLine += pArgs;
    _tsystem(sCommandLine.c_str());
}

static void DoRead(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoRead, aArgs)) return;

    if (aArgs.size() != 1) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    const TCHAR * pPath = aArgs[0];

    FILE * fp = _tfopen(pPath, _T("rb"));
#ifdef _WIN32
    // create a shared-read lock on this file to prevent writes to it while
    // we are reading it, as we use the file offsets for procedures
    HANDLE hLock = fp ? CreateFile(pPath, GENERIC_READ, FILE_SHARE_READ,
        NULL, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, NULL) : INVALID_HANDLE_VALUE;
    if (fp && hLock == INVALID_HANDLE_VALUE) {
        fclose(fp);
        fp = NULL;
    }
#endif
    if (!fp) {
        _ftprintf(stderr, _T("Error opening file '%s'\n"), pPath);
        return;
    }

    // see if we have a UTF-8 file signature
    bool bIsUtf8 = false;
    const char * szUtf8Signature = "\xEF\xBB\xBF";
    char szSig[3];
    if (3 == fread(szSig, 1, 3, fp) && 0 == memcmp(szSig, szUtf8Signature, 3)) {
        bIsUtf8 = true;
    }
    else {
        fseek(fp, 0, SEEK_SET);
    }

    aThread->mInputStack.push_back(aThread->mInputCurr);
    aThread->mInputCurr = InputFile(fp, pPath, bIsUtf8);
#ifdef _WIN32
    // ensure that we have a read lock on this file
    aThread->mInputCurr.mFileLock = hLock;
#endif
}

#ifndef MSYS_NOTHREADS
static void ThreadCreate(MSYS_ThreadImpl * aThread, TCHAR * aThreadName, TCHAR * aPath)
{
    if (!aThreadName || !aPath) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    if (*aThreadName) {
        ThreadMap::const_iterator i = g_mapThreads.find(aThreadName);
        if (i != g_mapThreads.end()) {
            MSYS_Display(aThread, _T("Thread '%s' is already registered.\n"), aThreadName);
            return;
        }
    }

    // create our thread
    MSYS_ThreadImpl * pNewThread = new MSYS_ThreadImpl(aThreadName, aThread);
    if (!pNewThread) {
        _ftprintf(stderr, _T("Out of memory.\n"));
        exit(1);
    }
    MSYS_ThreadUserCreate(pNewThread, aThread);

    // generate a thread name if necessary
    if (!*aThreadName) {
        TCHAR szThreadId[100];
        _sntprintf(szThreadId, sizeof(szThreadId)/sizeof(szThreadId[0]),
            _T("%d"), pNewThread->GetThreadId());
        ThreadMap::const_iterator i = g_mapThreads.find(szThreadId);
        for (int nCount = 0; i != g_mapThreads.end(); ++nCount) {
            _sntprintf(szThreadId, sizeof(szThreadId)/sizeof(szThreadId[0]),
                _T("%d-%d"), pNewThread->GetThreadId(), nCount);
            i = g_mapThreads.find(szThreadId);
        }
        pNewThread->mThreadName = szThreadId;
        aThreadName = NULL;
    }

    // default thread output to none
    pNewThread->mOptionConsole = DISPLAY_NONE;

    // start this thread reading input on the file
    MSYS_Args args;
    args.push_back(aPath);
    DoRead(pNewThread, args);
    if (pNewThread->mInputCurr.mFile == stdin) { // failed
        delete pNewThread;
        return;
    }
    pNewThread->mInputStack.pop_back(); // don't return to stdin

    // start a real thread executing
#ifdef _WIN32
    unsigned threadID;
    pNewThread->mThreadHandle = (HANDLE) _beginthreadex(
        NULL, 0, InputLoop, pNewThread, 0, &threadID);
    if (!pNewThread->mThreadHandle) {
        delete pNewThread;
        _ftprintf(stderr, _T("Failed to start thread.\n"));
        return;
    }
#else
    int rc = pthread_create(&pNewThread->mThreadHandle,
        NULL, InputLoop, pNewThread);
    if (rc != 0) {
        delete pNewThread;
        _ftprintf(stderr, _T("Failed to start thread.\n"));
        return;
    }
#endif

    // add this thread handle to the thread table
    g_mapThreads.insert(ThreadMap::value_type(pNewThread->mThreadName, pNewThread));
}
#endif // MSYS_NOTHREADS

#ifndef MSYS_NOTHREADS
static void ThreadJoin(MSYS_ThreadImpl * aThread, TCHAR * aThreadName)
{
    if (aThreadName) {
        ThreadMap::const_iterator i = g_mapThreads.find(aThreadName);
        if (i == g_mapThreads.end()) {
            MSYS_Display(aThread, _T("Thread '%s' is not registered.\n"), aThreadName);
            return;
        }
        if (i->second == aThread) {
            MSYS_Display(aThread, _T("Cannot wait on self.\n"));
            return;
        }
    }
    if (g_mapThreads.empty()) {
        return;
    }

    ThreadMap::iterator i = g_mapThreads.begin();
    for (; i != g_mapThreads.end(); ++i) {
        MSYS_ThreadImpl * pThread = i->second;
        if (pThread == aThread) continue;
        if (aThreadName && pThread->mThreadName != aThreadName) continue;
#ifdef _WIN32
        DWORD dwResult = WaitForSingleObject(pThread->mThreadHandle, INFINITE);
        if (dwResult != WAIT_OBJECT_0) {
#else
        int rc = pthread_join(pThread->mThreadHandle, NULL);
        if (rc != 0) {
#endif
            MSYS_Display(aThread, _T("Wait for thread '%s' failed.\n"),
                pThread->mThreadName.c_str());
        }
        delete pThread;
        if (aThreadName) {
            g_mapThreads.erase(i);
            break;
        }
    }
    if (!aThreadName) {
        g_mapThreads.clear();
    }
}
#endif // MSYS_NOTHREADS

#ifndef MSYS_NOTHREADS
static void ThreadStop(MSYS_ThreadImpl * aThread, TCHAR * aThreadName)
{
    ThreadMap::const_iterator i = g_mapThreads.end();
    if (aThreadName) {
        i = g_mapThreads.find(aThreadName);
        if (i == g_mapThreads.end()) {
            MSYS_Display(aThread, _T("Thread '%s' is not registered.\n"), aThreadName);
            return;
        }
    }
    if (g_mapThreads.empty()) {
        return;
    }

    if (aThreadName) {
        i->second->bStopThread = true;
    }
    else {
        i = g_mapThreads.begin();
        for (int n = 0; i != g_mapThreads.end(); ++i, ++n) {
            i->second->bStopThread = true;
        }
    }
}
#endif // MSYS_NOTHREADS

#ifndef MSYS_NOTHREADS
static void DoThreads(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoThreads, aArgs)) return;

    if (!g_bEnableThreads) {
        MSYS_Display(aThread, _T("Threads are disabled.\n"));
        return;
    }
    AutoLock lock(g_lockThreadsMap);

    TCHAR * pAction = aArgs.size() >= 1 ? aArgs[0] : NULL;
    TCHAR * pName   = aArgs.size() >= 2 ? aArgs[1] : NULL;
    TCHAR * pPath   = aArgs.size() >= 3 ? aArgs[2] : NULL;

    // list
    if (!pAction || _tcsicmp(_T("list"), pAction) == 0) {
        if (g_mapThreads.empty()) {
            MSYS_Display(aThread, _T("No current threads.\n"));
            return;
        }

        ThreadMap::const_iterator i = g_mapThreads.begin();
        for (; i != g_mapThreads.end(); ++i) {
            const MSYS_ThreadImpl * pThread = i->second;
            MSYS_Display(aThread, _T("  %-2d %-12s %s\n"),
                pThread->GetThreadId(), i->first.c_str(),
                pThread->bIsRunning ? _T("running") : _T("done"));
        }
        return;
    }

    // start
    if (_tcsicmp(_T("start"), pAction) == 0) {
        ThreadCreate(aThread, pName, pPath);
        return;
    }

    // stop
    if (_tcsicmp(_T("stop"), pAction) == 0) {
        if (!pName) {
            MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
            return;
        }
        ThreadStop(aThread, pName);
        return;
    }

    // stopall
    if (_tcsicmp(_T("stopall"), pAction) == 0) {
        ThreadStop(aThread, NULL);
        return;
    }

    // join
    if (_tcsicmp(_T("join"), pAction) == 0) {
        if (!pName) {
            MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
            return;
        }
        ThreadJoin(aThread, pName);
        return;
    }

    // joinall
    if (_tcsicmp(_T("joinall"), pAction) == 0) {
        ThreadJoin(aThread, NULL);
        return;
    }

    MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
}
#endif // MSYS_NOTHREADS

static void DoTee(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoTee, aArgs)) return;

    const TCHAR * pPath = aArgs.empty() ? NULL : aArgs[0];
    const TCHAR * pMode = _T("wb");
    if (aArgs.size() == 2 && !_tcsicmp(aArgs[0], _T("-append"))) {
        pMode = _T("ab");
        pPath = aArgs[1];
    }

    FILE * fpNew = NULL;
    if (pPath) {
        fpNew = _tfopen(pPath, pMode);
        if (!fpNew) {
            _ftprintf(stderr, _T("Error opening file '%s'\n"), pPath);
            return;
        }
    }

    if (aThread->mTeeFile) {
        fclose(aThread->mTeeFile);
        aThread->mTeeFile = NULL;
        _ftprintf(stdout, _T("Closed tee output file\n"));
    }

    if (fpNew) {
        aThread->mTeeFile = fpNew;
        _ftprintf(stdout, _T("Started to tee output to '%s'\n"), pPath);
    }
}

static void
DisplayOption(
    MSYS_Thread *   aThread,
    const TCHAR *   aCommand,
    const TCHAR *   aKey,
    const TCHAR *   aVal
    )
{
    std::tstring sEscaped(aVal);
    std::tstring::size_type n = sEscaped.find_first_of(_T("\\\""));
    while (n != std::tstring::npos) {
        sEscaped.insert(n, _T("\\"));
        n = sEscaped.find_first_of(_T("\\\""), n + 2);
    }

    if (sEscaped.empty() || sEscaped.find(_T(' ')) != std::tstring::npos) {
        MSYS_Display(aThread, _T("%s %-12s \"%s\"\n"), aCommand, aKey, sEscaped.c_str());
    }
    else {
        MSYS_Display(aThread, _T("%s %-12s %s\n"), aCommand, aKey, sEscaped.c_str());
    }
}

extern void
MSYS_DisplayOption(
    MSYS_Thread *   aThread,
    const TCHAR *   aOption,
    const TCHAR *   aValue
    )
{
    if (!aOption || !aValue) return;
    DisplayOption(aThread, _T("setopt"), aOption, aValue);
}

static void DoSetVar(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoSetVar, aArgs, false)) {
        MSYS_ShowHelp(aThread, DoSetVar);
        return;
    }

    AutoLock lock(g_lockTimerAndVars);

    // display the current variables
    if (aArgs.empty()) {
        VarMap::iterator i = g_mapVariables.begin();
        for (; i != g_mapVariables.end(); ++i) {
            DisplayOption(aThread, _T("setvar"), i->first.c_str(), i->second.c_str());
        }
        return;
    }

    if (aArgs.size() != 2) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    std::tstring sKey(aArgs[0]);
    std::tstring sVal(aArgs[1]);
    std::pair<VarMap::iterator,bool> ib = g_mapVariables.insert(
        VarMap::value_type(sKey, sVal));
    if (!ib.second) ib.first->second = sVal;
}

static const TCHAR *
GetConsoleType(
    DISPLAY aType
    )
{
    switch (aType) {
    case DISPLAY_ALL:    return _T("all");
    case DISPLAY_PROMPT: return _T("prompt");
    case DISPLAY_OUTPUT: return _T("output");
    case DISPLAY_NONE:   return _T("none");
    default:             return NULL;
    }
}

static void DoSetOpt(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoSetOpt, aArgs, false)) {
        // determine if they have requested help for a specific option
        if (aArgs.size() == 2) {
            std::tstring sCommand = OPTION_PREFIX;
            sCommand += aArgs[0];
            for (int n = 0; g_rgDispatch[n].cmd; ++n) {
                if (0 != _tcsicmp(g_rgDispatch[n].cmd, sCommand.c_str())) continue;
                MSYS_ShowHelp(aThread, g_rgDispatch[n].fn);
                return;
            }
        }

        MSYS_ShowHelp(aThread, DoSetOpt);
        MSYS_Display(aThread, _T("\nAvailable options are:\n"));
        for (int n = 0; g_rgDispatch[n].cmd; ++n) {
            if (_tcsnicmp(g_rgDispatch[n].cmd, OPTION_PREFIX, OPTION_PREFIX_LEN)) continue;
            MSYS_Display(aThread, _T("  %-15s%s\n"),
                g_rgDispatch[n].cmd + OPTION_PREFIX_LEN, g_rgDispatch[n].oneHelp);
        }
        return;
    }

    std::tstring opt;
    std::tstring cmd;
    if (!aArgs.empty()) {
        opt = aArgs[0];
        cmd = OPTION_PREFIX;
        cmd += opt;
    }

    // load or save
    if (!aArgs.empty() && aArgs[0][0] == '-') {
        const TCHAR * pAction = aArgs[0];
        const TCHAR * pFile   = aArgs.size() > 1 ? aArgs[1] : MSYS_DefaultOptionsFile;

        if (!_tcsicmp(pAction, _T("-load"))) {
            MSYS_Args args;
            // DoRead never modifies the filename, so removing the const is safe...
            args.push_back(const_cast<TCHAR*>(pFile));
            DoRead(aThread, args);
            return;
        }

        if (!_tcsicmp(pAction, _T("-save"))) {
            // set tee to output file
            FILE * fpSave = aThread->mTeeFile;
            aThread->mTeeFile = _tfopen(pFile, _T("wb"));
            if (!aThread->mTeeFile) {
                aThread->mTeeFile = fpSave;
                MSYS_Display(aThread, _T("Failed to open options file.\n"));
                return;
            }

            DISPLAY nSave = aThread->mOptionConsole;
            aThread->mOptionConsole = DISPLAY_NONE;

            // disable the console and set UTF-8 when we reload this
            aThread->mHandlerBuf.Format(_T("%s"),
                _T("@setopt console none\n")
                _T("@setopt utf8 true\n")
                _T("\n")
                );
            OutputToFile(aThread, aThread->mTeeFile, aThread->mHandlerBuf);

            // output all USER options
            MSYS_Args args;
            args.resize(2);
            TCHAR szCommand[MAX_PATH+1];
            for (int n = 0; MSYS_Commands[n].cmd; ++n) {
                if (_tcsnicmp(MSYS_Commands[n].cmd, OPTION_PREFIX, OPTION_PREFIX_LEN) == 0) {
                    _tcsncpy(szCommand, MSYS_Commands[n].cmd + OPTION_PREFIX_LEN, MAX_PATH);
                    szCommand[MAX_PATH] = 0;
                    args[0] = _T("getopt");
                    args[1] = szCommand;
                    MSYS_Commands[n].fn(aThread, args);
                }
            }

            // output all variables
            args.clear();
            DoSetVar(aThread, args);

            // output the internal options. We can ignore utf8 because it will be reset
            // when the file read finishes. The threads option is read-only.
            MSYS_DisplayOption(aThread, _T("console"), GetConsoleType(nSave));

            aThread->mOptionConsole = nSave;
            fclose(aThread->mTeeFile);
            aThread->mTeeFile = fpSave;
            return;
        }
    }

    if (aArgs.size() < 2) {
        aArgs.insert(aArgs.begin(), _T("getopt"));
        if (aArgs.size() < 2) {
            aArgs.push_back(_T(""));
        }

        int nFound = 0;
        TCHAR szCommand[MAX_PATH+1];
        for (int n = 0; g_rgDispatch[n].cmd; ++n) {
            if (_tcsnicmp(g_rgDispatch[n].cmd, OPTION_PREFIX, OPTION_PREFIX_LEN) == 0) {
                if (!cmd.empty() && _tcsicmp(g_rgDispatch[n].cmd, cmd.c_str()) != 0) continue;
                _tcsncpy(szCommand, g_rgDispatch[n].cmd + OPTION_PREFIX_LEN, MAX_PATH);
                szCommand[MAX_PATH] = 0;
                aArgs[1] = szCommand;
                g_rgDispatch[n].fn(aThread, aArgs);
                ++nFound;
            }
        }

        if (aArgs.size() == 2 && !nFound) {
            MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
            return;
        }
    }
    else {
        aArgs.insert(aArgs.begin(), _T("setopt"));

        for (int n = 0; g_rgDispatch[n].cmd; ++n) {
            if (_tcsicmp(cmd.c_str(), g_rgDispatch[n].cmd) == 0) {
                g_rgDispatch[n].fn(aThread, aArgs);
                return;
            }
        }
        MSYS_Display(aThread, _T("Unrecognized option: %s\n"), opt.c_str());
    }
}

static void OptConsole(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, OptConsole, aArgs)) return;

    assert(!aArgs.empty());

    const TCHAR * pAction = aArgs[0];
    const TCHAR * pValue  = aArgs.size() > 2 ? aArgs[2] : NULL;

    // setopt
    if (*pAction == _T('s')) {
        TCHAR cValue = *pValue;
        if (cValue >= 'A' && cValue <= 'Z') {
            cValue -= 'A' + 'a'; // lowercase
        }
        switch (cValue) {
        case 'a': aThread->mOptionConsole = DISPLAY_ALL;    break;
        case 'p': aThread->mOptionConsole = DISPLAY_PROMPT; break;
        case 'o': aThread->mOptionConsole = DISPLAY_OUTPUT; break;
        case 'n': aThread->mOptionConsole = DISPLAY_NONE;   break;
        default:
            MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
            return;
        }
    }

    // getopt
    if (*pAction == _T('g')) {
        MSYS_DisplayOption(aThread, _T("console"),
            GetConsoleType(aThread->mOptionConsole));
    }
}

static void OptThreads(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, OptThreads, aArgs)) return;

    assert(!aArgs.empty());

    const TCHAR * pAction = aArgs[0];

    // setopt
    if (*pAction == _T('s')) {
        MSYS_Display(aThread, _T("This option is read-only and cannot be changed.\n"));
        return;
    }

    // getopt
    if (*pAction == _T('g')) {
        const TCHAR * pValue = g_bEnableThreads ? _T("true") : _T("false");
        MSYS_DisplayOption(aThread, _T("threads"), pValue);
    }
}

static void OptUtf8(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, OptUtf8, aArgs)) return;

    assert(!aArgs.empty());

    const TCHAR * pAction = aArgs[0];
    const TCHAR * pValue  = aArgs.size() > 2 ? aArgs[2] : NULL;

    // setopt
    if (*pAction == _T('s')) {
        aThread->mInputCurr.mIsUtf8 =
            pValue[0] == _T('t') || pValue[0] == _T('T') || pValue[0] == _T('1')
            || _tcsicmp(pValue, _T("on")) == 0;
    }

    // getopt
    if (*pAction == _T('g')) {
        const TCHAR * pValue = aThread->mInputCurr.mIsUtf8 ? _T("true") : _T("false");
        MSYS_DisplayOption(aThread, _T("utf8"), pValue);
    }
}

static void OptTimestamp(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, OptTimestamp, aArgs)) return;

    assert(!aArgs.empty());

    const TCHAR * pAction = aArgs[0];
    const TCHAR * pValue  = aArgs.size() > 2 ? aArgs[2] : NULL;

    // setopt
    if (*pAction == _T('s')) {
        aThread->mUseTimestamp =
            pValue[0] == _T('t') || pValue[0] == _T('T') || pValue[0] == _T('1')
            || _tcsicmp(pValue, _T("on")) == 0;
    }

    // getopt
    if (*pAction == _T('g')) {
        const TCHAR * pValue = aThread->mUseTimestamp ? _T("true") : _T("false");
        MSYS_DisplayOption(aThread, _T("timestamp"), pValue);
    }
}

static void DoProc(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoProc, aArgs)) return;

    if (aArgs.empty()) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    if (aThread->mInputCurr.mFile == stdin) {
        MSYS_Display(aThread, _T("Invalid during console input.\n"));
        return;
    }

    const TCHAR * pAction = aArgs[0];
    const TCHAR * pName   = aArgs.size() > 0 ? aArgs[1] : NULL;

    // begin (defines a procedure)
    if (_tcsicmp(pAction, _T("begin")) == 0) {
        long lProc = ftell(aThread->mInputCurr.mFile);
        bool bInserted = aThread->mInputCurr.mProc.insert(
            ProcMap::value_type(pName, lProc)).second;
        if (!bInserted) {
            MSYS_Display(aThread, _T("Ignoring duplicate procedure definition: \"%s\".\n"), pName);
        }

        // find the end of the procedure by searching for "proc end" in the file
        Buffer buf;
        TCHAR *pCommand, *pArgs;
        for (;;) {
            if (!Input(aThread, buf)) {
                MSYS_Display(aThread, _T("End of procedure definition not found: \"%s\".\n"), pName);
                break;
            }
            Parse(buf.mBuf, pCommand, pArgs);
            if (_tcsicmp(pCommand, _T("proc")) == 0 && _tcsicmp(pArgs, _T("end")) == 0) {
                break;
            }
        }
        return;
    }

    // end (finishes the current procedure and pops back to the original)
    if (_tcsicmp(pAction, _T("end")) == 0) {
        if (aThread->mInputCurr.mProcStack.empty()) {
            MSYS_Display(aThread, _T("Invalid return from procedure call.\n"));
            return;
        }

        // if we looped on this procedure then we're done
        if (aThread->Loop()) return;

        // otherwise pop the stack and return
        long lReturn = aThread->mInputCurr.mProcStack.back();
        aThread->mInputCurr.mProcStack.pop_back();
        if (fseek(aThread->mInputCurr.mFile, lReturn, SEEK_SET) != 0) {
            MSYS_Display(aThread, _T("Failed return from procedure call.\n"));
            return;
        }
        return;
    }

    // call a procedure
    if (_tcsicmp(pAction, _T("call")) == 0) {
        ProcMap::const_iterator i = aThread->mInputCurr.mProc.find(pName);
        if (i == aThread->mInputCurr.mProc.end()) {
            MSYS_Display(aThread, _T("Unknown procedure call: \"%s\".\n"), pName);
            return;
        }

        // push our return address
        long lCurr = ftell(aThread->mInputCurr.mFile);
        aThread->mInputCurr.mProcStack.push_back(lCurr);

        // call the procedure
        if (fseek(aThread->mInputCurr.mFile, i->second, SEEK_SET) != 0) {
            MSYS_Display(aThread, _T("Failed procedure call: \"%s\".\n"), pName);
        }
        return;
    }

    MSYS_Display(aThread, _T("Invalid command.\n"));
}

static int ParseTime(const TCHAR * aTimeString, struct tm & aTime)
{
    if (3 == _stscanf(aTimeString, _T("%u:%u:%u"),
        &aTime.tm_hour, &aTime.tm_min, &aTime.tm_sec))
    {
        return 3;
    }

    aTime.tm_hour = 0;
    if (2 == _stscanf(aTimeString, _T("%u:%u"),
        &aTime.tm_min, &aTime.tm_sec))
    {
        return 2;
    }

    aTime.tm_min = 0;
    if (1 == _stscanf(aTimeString, _T("%u"),
        &aTime.tm_sec))
    {
        return 1;
    }

    aTime.tm_sec = 0;
    return 0;
}

static void DoLoop(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoLoop, aArgs)) return;

    if (aArgs.size() != 3) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    if (aThread->mInputCurr.mFile == stdin) {
        MSYS_Display(aThread, _T("Invalid during console input.\n"));
        return;
    }

    const TCHAR * pProc  = aArgs[0];
    const TCHAR * pType  = aArgs[1];
    const TCHAR * pLimit = aArgs[2];

    ProcMap::const_iterator i = aThread->mInputCurr.mProc.find(pProc);
    if (i == aThread->mInputCurr.mProc.end()) {
        MSYS_Display(aThread, _T("Unknown procedure call: \"%s\".\n"), pProc);
        return;
    }

    // get our current location
    long lProcedure = i->second;

    // count
    if (_tcsicmp(_T("count"), pType) == 0) {
        int nCount = _ttoi(pLimit);
        aThread->LoopStartCounted(lProcedure, nCount);
        return;
    }

    // for (time period), until (time)
    if (_tcsicmp(_T("for"), pType) == 0 || _tcsicmp(_T("until"), pType) == 0) {
        time_t tNow, tLimit;
        time(&tNow);

        struct tm tmNow = *localtime(&tNow);
        int nTimeLen = ParseTime(pLimit, tmNow);

        if (_tcsicmp(_T("for"), pType) == 0) {
            // for (period)
            tLimit = tNow + tmNow.tm_sec + (tmNow.tm_min * 60) + (tmNow.tm_hour * 60 * 60);
        }
        else {
            // until (time)
            if (nTimeLen < 2) {
                MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
                return;
            }
            if (nTimeLen == 2) {
                tmNow.tm_hour = tmNow.tm_min;
                tmNow.tm_min  = tmNow.tm_sec;
                tmNow.tm_sec  = 0;
            }
            tLimit = mktime(&tmNow);

            // if it appears that the time is past, add a day to it
            if (tLimit < tNow) {
                tLimit += (24 /*hours*/ * 60 /*mins*/ * 60 /*secs*/);
            }
        }

        const time_t MAX_PERIOD = (7 * 24 * 60 * 60); // 7 days
        if ((tLimit - tNow) > MAX_PERIOD) {
            MSYS_Display(aThread, _T("Period too long. See help.\n"));
            return;
        }

        std::tstring sNow   = _tasctime(localtime(&tNow));
        std::tstring sLimit = _tasctime(localtime(&tLimit));
        MSYS_Display(aThread, 
            _T("Time now is:   %s")
            _T("Looping until: %s")
            _T("Looping for:   %d secs (%d h, %d m, %d s)\n"),
            sNow.c_str(), sLimit.c_str(),
            (tLimit - tNow),
            (tLimit - tNow) / (60 * 60),
            ((tLimit - tNow) / (60)) % 60,
            (tLimit - tNow) % (60)
            );

        // set the time loop parameters
        aThread->LoopStartTimed(lProcedure, tLimit);
        return;
    }

    MSYS_Display(aThread, _T("Invalid loop type.\n"));
}

static void TimerTell(MSYS_ThreadImpl * aThread, const TCHAR * aTimer, bool aShowRaw)
{
    AutoLock lock(g_lockTimerAndVars);

    if (g_mapTimers.empty()) {
        MSYS_Display(aThread, _T("No timers.\n"));
    }
    TimerMap::const_iterator iTimer = g_mapTimers.begin();
    for (; iTimer != g_mapTimers.end(); ++iTimer) {
        if (aTimer && iTimer->first != aTimer) continue;

        const Timer & timer = iTimer->second;
        long nElapsed = timer.GetCurrentElapsed();
        if (aShowRaw) {
            MSYS_Display(aThread, _T("\"timer\",\"%s\"\n"),
                iTimer->first.c_str());
            MSYS_Display(aThread, _T("\"current\",%ld.%03ld seconds\n"),
                nElapsed / 1000, nElapsed % 1000);
        }
        else {
            MSYS_Display(aThread, _T("%c%-10s %5ld.%03ld seconds\n"),
                timer.IsRunning() ? '*' : ' ',
                iTimer->first.c_str(), nElapsed / 1000, nElapsed % 1000);
        }
        if (!timer.Laps().empty()) {
            // display individual laps
            long nCount = (long) timer.Laps().size();
            long nTotal = 0;
            LapArray::const_iterator iLap = timer.Laps().begin();
            for (int n = 1; iLap != timer.Laps().end(); ++iLap, ++n) {
                nTotal += *iLap;
                if (aShowRaw) {
                    MSYS_Display(aThread, _T("%d,%ld.%03ld\n"),
                        n, *iLap / 1000, *iLap % 1000);
                }
                else {
                    MSYS_Display(aThread, _T("   %3d: %3ld.%03ld"),
                        n, *iLap / 1000, *iLap % 1000);
                    if (n % 5 == 0) MSYS_Display(aThread, _T("\n"));
                }
            }
            if (!aShowRaw && (nCount % 5) != 0) {
                MSYS_Display(aThread, _T("\n"));
            }

            if (aShowRaw) {
                MSYS_Display(aThread, _T("\n"));
            }
            else {
                // display actual total and average
                long nAverage = nTotal / nCount;
                MSYS_Display(aThread, _T("   Total: %ld.%03ld   Laps: %ld   Average: %ld.%03ld\n"),
                    nTotal / 1000, nTotal % 1000, nCount,
                    nAverage / 1000, nAverage % 1000);

                // generate an array removing 10% outriders from each side
                LapArray elim(timer.Laps());
                std::sort(elim.begin(), elim.end());
                long nOutrider = nCount / 10;
                elim.erase(elim.begin(), elim.begin() + nOutrider);
                elim.erase(elim.end() - nOutrider, elim.end());
                nTotal = 0;
                for (size_t n = 0; n < elim.size(); ++n) {
                    nTotal += elim[n];
                }
                nCount -= 2 * nOutrider;
                nAverage = nTotal / nCount;
                MSYS_Display(aThread, _T("   Total: %ld.%03ld   Laps: %ld   Average: %ld.%03ld (outrider adjusted)\n"),
                    nTotal / 1000, nTotal % 1000, nCount,
                    nAverage / 1000, nAverage % 1000);
            }
        }
    }
}

static void DoTimer(MSYS_Thread * thrd, MSYS_Args & aArgs)
{
    AutoLock lock(g_lockTimerAndVars);

    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    if (MSYS_NeedHelp(aThread, DoTimer, aArgs)) return;

    if (aArgs.size() > 2) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    const TCHAR * pAction = (aArgs.size() > 0) ? aArgs[0] : _T("tell");
    const TCHAR * pId     = (aArgs.size() > 1) ? aArgs[1] : NULL;

    // tell
    if (_tcsicmp(_T("tell"), pAction) == 0 || _tcsicmp(_T("tellraw"), pAction) == 0) {
        bool bIsRaw = (pAction[4] != 0);
        TimerTell(aThread, pId, bIsRaw);
        return;
    }

    if (!pId) {
        MSYS_Display(aThread, _T("Invalid arguments. See help.\n"));
        return;
    }

    // lookup this timer
    TimerMap::iterator iTimer = g_mapTimers.find(pId);

    // start
    if (_tcsicmp(_T("start"), pAction) == 0) {
        // we only create a timer on "start"
        if (iTimer == g_mapTimers.end()) {
            iTimer = g_mapTimers.insert(
                TimerMap::value_type(pId, Timer())).first;
        }
        iTimer->second.Start();
        return;
    }

    // lap
    if (_tcsicmp(_T("lap"), pAction) == 0) {
        if (iTimer == g_mapTimers.end()) {
            iTimer = g_mapTimers.insert(
                TimerMap::value_type(pId, Timer())).first;
            iTimer->second.Start();
        }
        else {
            iTimer->second.Lap();
        }
        return;
    }

    // delete
    if (_tcsicmp(_T("delete"), pAction) == 0) {
        if (iTimer != g_mapTimers.end()) {
            g_mapTimers.erase(iTimer);
        }
        return;
    }

    if (iTimer == g_mapTimers.end()) {
        MSYS_Display(aThread, _T("Invalid timer.\n"));
        return;
    }

    // stop
    if (_tcsicmp(_T("stop"), pAction) == 0) {
        iTimer->second.Stop();
        return;
    }

    // reset
    if (_tcsicmp(_T("reset"), pAction) == 0) {
        iTimer->second.Reset();
        return;
    }

    MSYS_Display(aThread, _T("Invalid command.\n"));
}

static bool
Dispatch(
    MSYS_ThreadImpl *   aThread,
    TCHAR *             aCommand,
    TCHAR *             aArgs,
    PARSE               aParse
    )
{
    MSYS_Args args;

    // special handling for shell
    if (*aCommand == '!' || !_tcsicmp(aCommand, _T("shell"))) {
        if (*aCommand == '!') {
            ++aCommand; // may be a prefixed entry
        }
        else {
            aCommand = _T("");
        }
        args.push_back(aCommand);
        args.push_back(aArgs);
        DoShell(aThread, args);
        return true;
    }

    if (*aCommand != _T('_')) {
        for (int n = 0; g_rgDispatch[n].cmd; ++n) {
            if (_tcsicmp(aCommand, g_rgDispatch[n].cmd) == 0) {
                bool bFullParse = (aParse != PARSE_OFF) &&
                    ((g_rgDispatch[n].flags & MSYS_UNQUOTE) || (aParse == PARSE_ON));
                ParseArgs(aArgs, bFullParse, args);
                aThread->SetCurrCommand(g_rgDispatch[n].cmd);
                g_rgDispatch[n].fn(aThread, args);
                aThread->SetCurrCommand(NULL);
                return (g_rgDispatch[n].fn != DoExit);
            }
        }
    }
    MSYS_Display(aThread, _T("Unrecognized command: %s\n"), aCommand);
    return true;
}

static void InitInternal()
{
    // count the total number of commands
    int nInternal = 0, nExternal = 0;
    for (int n = 0; g_rgInternal[n].cmd; ++n) ++nInternal;
    for (int n = 0; MSYS_Commands[n].cmd; ++n) {
        const MSYS_DispatchEntry * pItem = std::find(
            &g_rgInternal[0], &g_rgInternal[nInternal], MSYS_Commands[n]);
        if (pItem->cmd != MSYS_HEADER && pItem != &g_rgInternal[nInternal]) {
            _ftprintf(stderr,
                _T("Command '%s' is shadowed by internal command of the same name\n"),
                pItem->cmd);
            assert(!"Command shadowed by internal command.");
        }
        ++nExternal;
    }

    // allocate our combined table
    g_rgDispatch = (MSYS_DispatchEntry*)
        calloc(sizeof(MSYS_DispatchEntry), nInternal+nExternal+1);
    if (!g_rgDispatch) {
        _ftprintf(stderr, _T("Out of memory.\n"));
        exit(1);
    }

    // create our combined table
    for (int n = 0; n < nInternal; ++n) {
        g_rgDispatch[n] = g_rgInternal[n];
    }
    for (int n = 0; n < nExternal; ++n) {
        g_rgDispatch[nInternal + n] = MSYS_Commands[n];
    }
}

static bool
FileExists(
    const TCHAR * aFilePath
    )
{
#ifdef _WIN32
    DWORD dwAttribs = GetFileAttributes(aFilePath);
    if (dwAttribs == INVALID_FILE_ATTRIBUTES) {
        return false;
    }
    if (dwAttribs & FILE_ATTRIBUTE_DIRECTORY) {
        return false;
    }
    return true;
#else
    struct stat st;
    if (-1 == stat(aFilePath, &st)) {
        return false;
    }
    if (st.st_mode & S_IFDIR) {
        return false;
    }
    return true;
#endif
}

#ifdef _WIN32
void
InputFromConsole(
    Buffer & aBuf
    )
{
    // we can't use simple pipes because using ReadFile on the console doesn't
    // work to convert the input characters to Unicode.

    int nLen = 0;
    std::wstring sLine;
    aBuf.mBuf[0] = 0;
    for (;;) {
        ReadConsole(GetStdHandle(STD_INPUT_HANDLE),
            aBuf.mBuf, (DWORD) aBuf.mBufSiz-1, (DWORD*) &nLen, NULL);
        aBuf.mBuf[nLen] = 0;

        // trim trailing whitespace from the line
        for (--nLen; nLen >= 0 && _istspace(aBuf.mBuf[nLen]); ) {
            aBuf.mBuf[nLen--] = 0;
        }

        // check for continuation char
        if (nLen >= 0 && aBuf.mBuf[nLen] == '\\') {
            aBuf.mBuf[nLen--] = 0;
            sLine += aBuf.mBuf;
            _ftprintf(stdout, _T("_ "));
            continue;
        }
        break;
    }

    // create the full line
    if (!sLine.empty()) {
        sLine += aBuf.mBuf;
        while (sLine.size() >= aBuf.mBufSiz) {
            aBuf.Grow();
        }
        wcscpy(aBuf.mBuf, sLine.c_str());
    }
}
#endif

void
OutputToConsole(
    const TCHAR * aString
    )
{
    AutoLock lock(g_lockOutputConsole);

#ifdef _WIN32
    // we can't use simple pipes because using WriteFile on the console doesn't work
    // to convert the Unicode characters correctly.
    DWORD dwWritten;
    const DWORD CHUNK = 10000;
    DWORD nLength = (DWORD) _tcslen(aString);
    for (DWORD nOffset = 0; nOffset < nLength; nOffset += CHUNK) {
        DWORD nOutputLen = nLength - nOffset > CHUNK ? CHUNK : nLength - nOffset;
        static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
        WriteConsole(hStdOut, aString + nOffset, nOutputLen, &dwWritten, NULL);
    }
#else
    _ftprintf(stdout, "%s", aString);
#endif
}

#ifdef _WIN32
extern void
MSYS_DisplayWindowsError(
    MSYS_Thread *   aThread,
    const TCHAR *   aLocation,
    HRESULT         aError
    )
{
    DWORD dwFormatFlags =
        FORMAT_MESSAGE_IGNORE_INSERTS |
        FORMAT_MESSAGE_FROM_SYSTEM;

    TCHAR szMessage[1000];
    DWORD dwResult = FormatMessage(
        dwFormatFlags,
        NULL,
        aError,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        szMessage,
        sizeof(szMessage)/sizeof(szMessage[0]),
        NULL );

    MSYS_Display(aThread, _T("%s failed with hr = 0x%08x, %s"),
        aLocation, aError,
        dwResult ? szMessage : _T("<failed to get error string>") );
}
#endif

const MSYS_DispatchEntry g_rgInternal[] = {
    // general
    { MSYS_HEADER, NULL, 0, _T("GENERAL"), NULL },
    { _T("?"),      DoHelp,     0,  NULL, NULL },
    { _T("help"),   DoHelp,     0,  _T("Display command help. Use \"help /?\" for more details."),
        _T("Usage: help [<command> | /internal | /all]\n")
        _T("\n")
        _T("    <command>   Show detailed help for a specific command. By default only\n")
        _T("                summary help is shown for commands. Use \"help <command>\" or\n")
        _T("                \"<command> -?\" for full help for that command.\n")
        _T("    /internal   Show help for internal commands. By default only external\n")
        _T("                commands are shown in the command list.\n")
        _T("    /all        Show help for both internal and external commands.\n")
        _T("\n")
        _T("Note: the interpretation of an -? argument as a request for help can be disabled ")
        _T("so that it is passed through to the command by prefixing the command with the ")
        _T("'-' character.")
    },

    { _T("x"),      DoExit,     0,  NULL, NULL },
    { _T("exit"),   DoExit,     0,  _T("Exit the application."),
        _T("By default the exit command will only exit the currently executing thread. ")
        _T("If the current thread is the main thread, it will wait for all other threads ")
        _T("to exit before returning. To force an immediate exit from all threads, use ")
        _T("the command \"exit now\". This may be called from any executing thread.\n")
        _T("\n")
        _T("Usage: exit [now]")
    },
    { _T("setopt"), DoSetOpt,   0,  _T("Set or display options."),
        _T("Usage: setopt [<option>] [<value>]\n")
        _T("\n")
        _T("If no option or value to set is supplied the current values will be displayed. ")
        _T("Use \"setopt <option> -?\" for help on available options.\n")
        _T("\n")
        _T("Usage: setopt {-save|-load} [<file>]\n")
        _T("\n")
        _T("Save or load options with an options file. If no file name is supplied ")
        _T("then a default filename will be used.")
    },

    // file system
    { MSYS_HEADER, NULL, 0, _T("FILE SYSTEM"), NULL },
    { _T("pwd"),    DoPwd,      0,  _T("Display the current working directory."),
        NULL
    },
    { _T("cd"),     DoChdir,    0,  _T("Change the current working directory."),
        _T("Usage: cd <path>")
    },
    { _T("!"),      DoShell,    0,  NULL, NULL },
    { _T("shell"),  DoShell,    0,  _T("Execute a shell command."),
        _T("If no command is supplied then a shell will be started. All characters are passed to the shell ")
        _T("unchanged (no quoted characters are unquoted).\n")
        _T("\n")
        _T("Usage: shell [<command> [<arguments>]]")
    },

    // input and output
    { MSYS_HEADER, NULL, 0, _T("INPUT AND OUTPUT"), NULL },
    { _T("e"),      DoEcho,     MSYS_UNQUOTE,   NULL, NULL },
    { _T("echo"),   DoEcho,     MSYS_UNQUOTE,   _T("Echo all arguments."),
        _T("Each argument is echoed with all quoted characters (e.g. \\t \\n \\\" etc) converted. ")
        _T("Arguments are echoed immediately following each other, so to echo a full line of text, ")
        _T("start the line with a quotation mark. For example:\n")
        _T("\n")
        _T("echo \"This full line will be echoed.")
    },
    { _T("r"),      DoRead,     0,  NULL, NULL },
    { _T("read"),   DoRead,     0,  _T("Read and execute a script file."),
        _T("Usage: read <path>")
    },
    { _T("tee"),    DoTee,      0,  _T("Copy all output to a file."),
        _T("When a tee has been created, all output that is sent to the screen is also written ")
        _T("to the output file. The tee file is created by overwriting any existing file by ")
        _T("default. Specify \"-append\" to force data to be appended to the tee file. ")
        _T("Stop output from being written to the tee file by omitting the file path. ")
        _T("Shell command output cannot be redirected with a tee.\n")
        _T("\n")
        _T("Usage: tee [-append] [<path>]")
    },

    // scripting
    { MSYS_HEADER, NULL, 0, _T("SCRIPTING"), NULL },
    { _T("setvar"), DoSetVar,   0,  _T("Set or display variables."),
        _T("Usage: setvar [<variable> <value>]\n")
        _T("\n")
        _T("If no variable is supplied the current variables will be displayed. ")
        _T("All current variables will be saved to the same file as the setopt options. ")
        _T("All variables are shared by all threads. To create a thread local variable, ")
        _T("use the name or ID of the thread in the variable name. ")
    },
    { _T("timer"),  DoTimer,    0,  _T("Start, stop and display timers."),
        _T("If no arguments are given, all timers are reported.\n")
        _T("\n")
        _T("Usage: timer [<command> <id>]\n")
        _T("\n")
        _T("  start    Create a new timer running or restart an existing one.\n")
        _T("  stop     Stop an existing timer from running. The timer is not deleted.\n")
        _T("  lap      Store the current time as a lap and start a new lap. If the timer\n")
        _T("           doesn't exist this is the same as Start().\n")
        _T("  reset    Reset the current elapsed time and all lap details of the timer.\n")
        _T("  tell     Display the current timer details.\n")
        _T("  tellraw  Display the current timer details in raw format.\n")
        _T("  delete   Delete an existing timer.")
    },
    { _T("sleep"),  DoSleep,    0,  _T("Pause script execution for a period of time."),
        _T("Usage: sleep <milliseconds>")
    },
    { _T("proc"),   DoProc,     0,  _T("Start, end or call a procedure."),
        _T("This may only be used when reading from a file. The procedure name must not be ")
        _T("specified for \"proc end\".\n")
        _T("\n")
        _T("Usage: proc {begin|end|call} [<name>]")
    },
    { _T("loop"),   DoLoop,     0,  _T("Repetitively call a procedure."),
        _T("The number of times the procedure will be called depends on the type of ")
        _T("limit that is created. A procedure name always needs to be supplied. The ")
        _T("<limit> text depends on the <type> that is specified. When using \"until\", ")
        _T("the time is specified using the 24 hour clock. Times in the past are assumed ")
        _T("to be 24 hours in the future. When using \"for\", the maximum period that can ")
        _T("be specified is 7 days.\n")
        _T("\n")
        _T("Usage: loop <proc-name> <type> <limit>\n")
        _T("\n")
        _T("  count <iterations>    Loop a fixed number of times.\n")
        _T("  until HH:MM[:SS]      Loop until the specified time (24 hour time).\n")
        _T("  for [[HH:]MM:]SS      Loop for the specified period of time.")
    },
#ifndef MSYS_NOTHREADS
    { _T("threads"), DoThreads, 0,  _T("Manage execution threads."),
        _T("Threads must always execute on an input file. By default, a new thread will start ")
        _T("with console display set to \"off\". If threads are not supported by the application ")
        _T("then this command will return an error. If no name is supplied for a new thread then ")
        _T("a name will be generated from the thread ID (e.g. threads start \"\" file.txt).\n")
        _T("\n")
        _T("Usage: threads <command> [<name> [<path>]]\n")
        _T("\n")
        _T("  start <name> <path>  Create a new thread with <name> using file <path>.\n")
        _T("  stop <name>          Stop the thread with <name> immediately.\n")
        _T("  stopall              Stop all threads immediately.\n")
        _T("  join <name>          Join with the thread with <name>.\n")
        _T("  joinall              Join with all threads.\n")
        _T("  list                 List all threads.")
    },
#endif // MSYS_NOTHREADS

    // options
    { _T("_setopt_console"), OptConsole, 0, _T("Enable or disable console display."),
        _T("The display of command output and the echoing of the command prompt can be ")
        _T("selectively disabled. For example, if input is from a file and is being tee'd ")
        _T("to an output file, all console output can be disabled so that only the tee file ")
        _T("receives the output. Note that disabling command prompt echo only affects the ")
        _T("echoing of command prompt during file input. Standard console input will always ")
        _T("display the command prompt. Shell output cannot be disabled (or tee'd).\n")
        _T("\n")
        _T("Usage: setopt console {all|prompt|output|none}\n")
        _T("\n")
        _T("  all     Command prompt: echoed. Command output: echoed.\n")
        _T("  prompt  Command prompt: echoed. Command output: none.\n")
        _T("  output  Command prompt: none.   Command output: echoed.\n")
        _T("  none    Command prompt: none.   Command output: none.\n")
    },
    { _T("_setopt_threads"), OptThreads, 0, _T("Are threads enabled (read-only)."),
        NULL
    },
    { _T("_setopt_utf8"), OptUtf8, 0, _T("Force file input to use UTF-8 encoding."),
        _T("Usage: setopt utf8 {true|false}")
    },
    { _T("_setopt_timestamp"), OptTimestamp, 0, _T("Display timestamps for each command"), 
        _T("Usage: setopt timestamp {true|false}")
    },

    { NULL, NULL, 0, NULL, NULL }
};

extern void MSYS_ShowHelp(MSYS_Thread * thrd, MSYS_Handler fn)
{
    MSYS_ThreadImpl * aThread = reinterpret_cast<MSYS_ThreadImpl*>(thrd);

    bool bSaved = aThread->mStringReplacements;
    aThread->mStringReplacements = false;

    std::tstring sCommands;

    bool bDisplayInternal = false;
    const MSYS_DispatchEntry * rgDispatch = MSYS_Commands;
    if (fn == HelpAll) {
        rgDispatch = g_rgDispatch;
        fn = NULL;
        bDisplayInternal = true;
    }
    else if (fn == HelpInternal) {
        rgDispatch = g_rgInternal;
        fn = NULL;
        bDisplayInternal = true;
    }
    else if (fn == HelpExternal) {
        fn = NULL;
    }

    // general help
    if (!fn) {
        bool bIsFirst = true;

        // we always display the general group of the internal commands
        int nSwitchHeader = 0;
        if (!bDisplayInternal) {
            rgDispatch = g_rgInternal;
            nSwitchHeader = 2;
        }

        for (int n = 0; rgDispatch[n].cmd; ++n) {
            if (rgDispatch[n].cmd[0] == _T('_')) continue;
            if (rgDispatch[n].flags & MSYS_HIDDEN) continue;
            if (rgDispatch[n].cmd == MSYS_HEADER) {
                if (--nSwitchHeader == 0) {
                    assert(!bDisplayInternal && rgDispatch == g_rgInternal);
                    rgDispatch = MSYS_Commands;
                    n = -1; // so we start at 0 after continue
                    continue;
                }

                MSYS_Display(aThread, _T("%s%s\n"),
                    bIsFirst ? _T("") : _T("\n"),
                    rgDispatch[n].oneHelp);
                bIsFirst = false;
                continue;
            }

            if (!sCommands.empty()) {
                sCommands += _T(", ");
            }
            sCommands += rgDispatch[n].cmd;
            if (!rgDispatch[n].oneHelp) continue;

            MSYS_Display(aThread, _T("%-15s%s\n"),
                sCommands.c_str(), rgDispatch[n].oneHelp);
            sCommands.erase();
            bIsFirst = false;
        }

        if (bDisplayInternal) {
            MSYS_Display(aThread, _T("%s"),
                _T("\n")
                _T("Special line prefixes to change default behaviour are:\n")
                _T("  ;   Single line comment.\n")
                _T("  #   Single line comment.\n")
                _T("  @   Suppress the echoing of the prompt and command.\n")
                _T("  $   Report the execution time of the command.\n")
                _T("  -   Force off the parsing of special characters.\n")
                _T("  +   Force on the parsing of special characters.\n")
                _T("  =   Prevent \"-?\" or \"/?\" from being interpreted as a help request.\n")
                _T("  !   Execute the command as a shell command.\n")
                _T("  /*  Start multiple line comment (must be the only text on a line).\n")
                _T("  */  End multiple line comment (must be the only text on a line).\n")
                _T("\n")
                _T("Variables:\n")
                _T("  {{name}}      Any variable set with setvar can be specified.\n")
                _T("  {{NOW}}       Special variable. Current time and date.\n")
                _T("  {{TID}}       Special variable. Current thread ID.\n")
                _T("  {{TNAME}}     Special variable. Current thread name.\n")
                );
        }
    }
    else {
        // specific command help
        for (int n = 0; g_rgDispatch[n].cmd; ++n) {
            if (g_rgDispatch[n].fn != fn) continue;
            if (g_rgDispatch[n].flags & MSYS_HIDDEN) continue;
            if (!sCommands.empty()) {
                sCommands += _T(", ");
            }
            if (0 == _tcsnicmp(g_rgDispatch[n].cmd, OPTION_PREFIX, OPTION_PREFIX_LEN)) {
                sCommands += _T("setopt ");
                sCommands += (g_rgDispatch[n].cmd + OPTION_PREFIX_LEN);
            }
            else {
                sCommands += g_rgDispatch[n].cmd;
            }
            if (!g_rgDispatch[n].oneHelp) continue;

            MSYS_Display(aThread, _T("Commands:  %s\nSummary:   %s\n"),
                sCommands.c_str(), g_rgDispatch[n].oneHelp);
            if (g_rgDispatch[n].longHelp) {
                MSYS_Display(aThread, _T("\n"));
                MSYS_DisplayWrapped(aThread, g_rgDispatch[n].longHelp);
            }
            break;
        }
    }

    aThread->mStringReplacements = bSaved;
}

